package org.tamal.java;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * This class will find classes.
 * @author Tamal Kanti Nath
 * @deprecated memory leakage in PermGenSpace.
 */
@Deprecated
public final class FindClass {

    private static final String CLASS_EXT = ".class";
    private static Map<String, Class<?>> classes = new TreeMap<>();

    static {
        init();
    }

    private FindClass() {
    }

    /**
     * Checks if the identifier is valid or not.
     * @param identifier the identifier name
     * @return true if valid, false otherwise
     */
    public static boolean isValidIdentifier(String identifier) {
        final String[] reserved = {"abstract", "assert", "boolean", "break",
            "byte", "case", "catch", "char", "class", "const", "continue",
            "default", "do", "double", "else", "enum", "extends", "false",
            "final", "finally", "float", "for", "goto", "if", "implements",
            "import", "instanceof", "int", "interface", "long", "native", "new",
            "null", "package", "private", "protected", "public", "return",
            "short", "static", "strictfp", "super", "switch", "synchronized",
            "this", "throw", "throws", "transient", "true", "try", "void",
            "volatile", "while", };
        if (!Character.isJavaIdentifierStart(identifier.charAt(0))) {
            return false;
        }
        for (int i = 1; i < identifier.length(); ++i) {
            if (!Character.isJavaIdentifierPart(identifier.charAt(i))) {
                return false;
            }
        }
        for (String word : reserved) {
            if (word.equals(identifier)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Initialize the class with boot, extended and system class path. The class
     * path is initialized by default and should exit silently if classes are
     * already available.
     */
    public static void init() {
        if (!classes.isEmpty()) {
            return;
        }
        String sep = System.getProperty("path.separator");
        Set<String> path = new TreeSet<>();
        String[] cp = System.getProperty("sun.boot.class.path").split(sep);
        path.addAll(Arrays.asList(cp));

        for (String dir: System.getProperty("java.ext.dirs").split(sep)) {
            cp = new File(dir).list();
            if (cp != null) {
                path.addAll(Arrays.asList(cp));
            }
        }

        cp = System.getProperty("java.class.path").split(sep);
        path.addAll(Arrays.asList(cp));

        for (String fileName : path) {
            try {
                addJar(fileName);
            } catch (IOException e) {
                addClasses(fileName);
            }
        }
    }

    /**
     * clears the classes map.
     */
    public static void clear() {
        classes.clear();
    }

    /**
     * Make a jar visible to FindClass.
     * @param dir The class path
     */
    public static void addClasses(String dir) {
        File file = new File(dir);
        if (file.isDirectory()) {
            addClasses(file, null);
        }
    }

    private static void addClasses(File dir, String pk) {
        String pkg = (pk == null || pk.isEmpty()) ? "" : pk + '.';
        for (File file: dir.listFiles()) {
            String name = file.getName();
            if (file.isFile() && name.endsWith(CLASS_EXT)) {
                name = name.substring(0, name.length() - 6);
                if (isValidIdentifier(name)) {
                    addClass(pkg + name);
                }
            }
            if (file.isDirectory() && isValidIdentifier(file.getName())) {
                addClasses(file, pkg + file.getName());
            }
        }
    }

    /**
     * Make a jar visible to FindClass.
     * @param fileName the jar file
     * @throws IOException if jar file is not a valid jar file.
     */
    public static void addJar(String fileName) throws IOException {
        try (JarFile file = new JarFile(fileName)) {
            Enumeration<JarEntry> entries = file.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.isDirectory()) {
                    continue;
                }
                if (entry.getName().endsWith(CLASS_EXT)) {
                    String name = entry.getName();
                    name = name.substring(0, name.length() - 6);
                    name = name.replace('/', '.');
                    boolean valid = true;
                    for (String identifier : name.split(".")) {
                        if (!isValidIdentifier(identifier)) {
                            valid = false;
                            break;
                        }
                    }
                    if (valid) {
                        addClass(name);
                    }
                }
            }
        }
    }

    private static void addClass(String name) {
        try {
            classes.put(name, Class.forName(name, false,
                    FindClass.class.getClassLoader()));
        } catch (ClassNotFoundException e) {
            System.err.println(e);
        } catch (NoClassDefFoundError e) {
            System.err.println(e);
        }
    }

    /**
     * Finds classes matching name.
     * If the regex is null or empty then all classes shall be returned.
     * @param regex the class name pattern
     * @return List of class names
     */
    public static List<Class<?>> getClasses(String regex) {
        List<Class<?>> match = new ArrayList<>();
        if (regex == null || regex.isEmpty()) {
            match.addAll(classes.values());
            return match;
        }
        for (Map.Entry<String, Class<?>> entry : classes.entrySet()) {
            if (entry.getKey().matches(regex)) {
                match.add(classes.get(entry.getValue()));
            }
        }
        return match;
    }

    /**
     * Returns declared subclasses of a class.
     * @param cls The superclass
     * @return list of subclasses
     */
    public static List<Class<?>> getDeclaredSubclasses(Class<?> cls) {
        List<Class<?>> match = new ArrayList<>();
        for (Class<?> each : classes.values()) {
            if (!cls.isAssignableFrom(each) || cls.equals(each)) {
                continue;
            }
            if (cls.isInterface() && each.isInterface()) {
                for (Class<?> c : each.getInterfaces()) {
                    if (cls.equals(c)) {
                        match.add(each);
                        break;
                    }
                }
            }
            if (!cls.isInterface() && !each.isInterface()) {
                if (cls.equals(each.getSuperclass())) {
                    match.add(each);
                }
            }
        }
        return match;
    }

    /**
     * Returns subclasses of a class.
     * @param cls The superclass
     * @return list of subclasses
     */
    public static List<Class<?>> getSubclasses(Class<?> cls) {
        List<Class<?>> match = new ArrayList<>();
        for (Class<?> each : classes.values()) {
            if (!cls.isAssignableFrom(each) || cls.equals(each)) {
                continue;
            }
            if ((cls.isInterface() && each.isInterface())
                    || (!cls.isInterface() && !each.isInterface())) {
                match.add(each);
            }
        }
        return match;
    }

    /**
     * Returns classes that implements an interface directly.
     * @param cls The Interface
     * @return list of implemented classes
     */
    public static List<Class<?>> getDeclaredImplements(Class<?> cls) {
        List<Class<?>> match = new ArrayList<>();
        if (!cls.isInterface()) {
            return match;
        }
        for (Class<?> each : classes.values()) {
            if (each.isInterface() || !cls.isAssignableFrom(each)) {
                continue;
            }
            for (Class<?> c : each.getInterfaces()) {
                if (cls.equals(c)) {
                    match.add(each);
                    break;
                }
            }
        }
        return match;
    }

    /**
     * Returns classes that implements an interface.
     * @param cls The Interface
     * @return list of implemented classes
     */
    public static List<Class<?>> getImplements(Class<?> cls) {
        List<Class<?>> match = new ArrayList<>();
        if (!cls.isInterface()) {
            return match;
        }
        for (Class<?> each : classes.values()) {
            if (each.isInterface() || !cls.isAssignableFrom(each)) {
                continue;
            }
            match.add(each);
        }
        return match;
    }
}
